__all__ = []

import argparse
import datetime
import getpass
import hashlib
import json
import logging
import os
import platform
import random
import re
import shutil
import subprocess
import sys
import tempfile
import time
import urllib.request
import uuid


__author__  = 'ZL <837806295@qq.com>'
__product__ = 'util'
__version__ = '1.1.25'
__date__    = '2023/04/08'


log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler())  # 如果宿主未配置日志处理器，则丢弃日志


def set_log(launch_path, log_name, logger_level, log_identifier='',
            root_dir='log'):
    """设置文件日志
    """

    if (not isinstance(launch_path, str)
            or not launch_path
            or not os.path.isfile(launch_path)
            or not isinstance(log_name, str)
            or not log_name
            or not is_identifier_ascii(log_name)
            or not isinstance(logger_level, str)
            or not isinstance(log_identifier, str)
            or (log_identifier and not is_identifier_ascii(log_identifier))):
        sys.exit(1)

    log_level = {
        'debug': logging.DEBUG,
        'info': logging.INFO,
        'warning': logging.WARNING,
        'error': logging.ERROR,
        'critical': logging.CRITICAL,
    }

    if logger_level not in log_level.keys():
        sys.exit(1)

    launch_dir = os.path.split(os.path.realpath(launch_path))[0]
    log_dir = os.path.join(launch_dir, root_dir)

    if os.path.exists(log_dir):
        if not os.path.isdir(log_dir):
            try:
                os.remove(log_dir)
                os.mkdir(log_dir)
            except Exception:
                sys.exit(1)
    else:
        try:
            os.mkdir(log_dir)
        except Exception:
            sys.exit(1)

    if log_identifier:
        log_filename = ('%s_%s_%s_%s.log'
                        % (log_name,
                           datetime.datetime.now().strftime('%Y%m%d-%H%M%S'),
                           log_identifier,
                           uuid.uuid1().hex))
    else:
        log_filename = ('%s_%s_%s.log'
                        % (log_name,
                           datetime.datetime.now().strftime('%Y%m%d-%H%M%S'),
                           uuid.uuid1().hex))

    log_path = os.path.join(log_dir, log_filename)

    file_log = logging.FileHandler(log_path, encoding='utf-8')
    file_log.setFormatter(
        logging.Formatter('[%(asctime)s | %(name)s | %(filename)s:%(lineno)d | '
                          '%(process)d | %(thread)d | %(levelname)s]: '
                          '%(message)s'))

    root_logger = logging.getLogger()
    root_logger.setLevel(log_level[logger_level])
    root_logger.addHandler(file_log)

def get_launch_dir(launch_path):
    """获取运行目录

    从运行路径中获取其所在目录
    """

    if (not isinstance(launch_path, str)
            or not launch_path
            or not os.path.isfile(launch_path)):
        log.critical('get launch dir ... failed ... exit | illegal input')
        sys.exit(1)

    return os.path.split(os.path.realpath(launch_path))[0]

def get_test_subdir(launch_path, dir, root_dir='test'):
    """获取运行目录下测试子目录的子孙目录

    当是子目录时，给出子目录名，dir这时要是str类型，当是孙目录时要按顺序给出中间目录，这时dir
    是list类型

    不存在时会创建这些目录及中间目录
    """

    if (not isinstance(launch_path, str)
            or not launch_path
            or not os.path.isfile(launch_path)
            or (not isinstance(dir, str) and not isinstance(dir, list))):
        log.critical('get test subdir ... failed ... exit | illegal input')
        sys.exit(1)

    launch_dir = get_launch_dir(launch_path)

    try:
        if isinstance(dir, str) and dir:
            subdir = os.path.join(launch_dir, root_dir, dir)
        elif isinstance(dir, list) and dir:
            subdir = os.path.join(launch_dir, root_dir)
            for x in dir:
                if not isinstance(x, str) or not x:
                    log.critical('get test subdir ... failed ... exit')
                    sys.exit(1)

                subdir = os.path.join(subdir, x)
        else:
            subdir = os.path.join(launch_dir, root_dir)
    except Exception:
        log.exception('get test subdir ... exception ... exit')
        sys.exit(1)

    try:
        os.makedirs(subdir, exist_ok=True)
    except Exception:
        log.exception('get test subdir ... exception ... exit | makedirs')
        sys.exit(1)

    log.debug('get test subdir ... successed')
    return subdir

def remove_test_subdir(launch_path, dir, root_dir='test'):
    """移除运行目录下测试子目录的子孙目录
    """

    if (not isinstance(launch_path, str)
            or not launch_path
            or not os.path.isfile(launch_path)
            or (not isinstance(dir, str) and not isinstance(dir, list))
            or not dir):
        log.critical('remove test subdir ... failed ... exit | illegal input')
        sys.exit(1)

    launch_dir = get_launch_dir(launch_path)

    try:
        if isinstance(dir, str):
            subdir = os.path.join(launch_dir, root_dir, dir)
        elif isinstance(dir, list):
            subdir = os.path.join(launch_dir, root_dir)
            for x in dir:
                if not isinstance(x, str) or not x:
                    log.critical('remove test subdir ... failed ... exit')
                    sys.exit(1)

                subdir = os.path.join(subdir, x)
    except Exception:
        log.exception('remove test subdir ... exception ... exit')
        sys.exit(1)

    try:
        shutil.rmtree(subdir, ignore_errors=True)
    except Exception:
        log.exception('remove test subdir ... exception ... exit | rmtree')
        sys.exit(1)

    log.debug('remove test subdir ... successed')

def get_subdir(root_dir, *args):
    """获取指定目录的子孙目录

    不存在时会创建这些目录及中间目录
    """

    if (not isinstance(root_dir, str)
            or not root_dir
            or not os.path.isdir(root_dir)):
        log.critical('get subdir ... failed ... exit | illegal input')
        sys.exit(1)

    subdir = root_dir

    try:
        for x in args:
            if not isinstance(x, str) or not x:
                log.critical('get subdir ... failed ... exit')
                sys.exit(1)

            subdir = os.path.join(subdir, x)
    except Exception:
        log.exception('get subdir ... exception ... exit')
        sys.exit(1)

    try:
        os.makedirs(subdir, exist_ok=True)
    except Exception:
        log.exception('get subdir ... exception ... exit | makedirs')
        sys.exit(1)

    log.debug('get subdir ... successed')
    return subdir

def compare_files(path, path2, bytes_per_read=4*1024*1024):
    if (not isinstance(path, str)
            or not path
            or not os.path.isfile(path)
            or not isinstance(path2, str)
            or not path2
            or not os.path.isfile(path2)):
        log.critical('compare files ... error ... exit | illegal input')
        sys.exit(1)

    file_size = os.path.getsize(path)
    if file_size != os.path.getsize(path2):
        log.debug('compare files ... no | file size')
        return False

    log.debug('compare files ... ongoing | path: %s, path2: %s', path, path2)

    try:
        with open(path, 'rb') as f, open(path2, 'rb') as f2:
            if file_size < bytes_per_read:
                data = f.read()
                data2 = f2.read()
                equal = (data == data2)
            else:
                equal = True
                times = file_size // bytes_per_read
                for i in range(times):
                    data = f.read(bytes_per_read)
                    data2 = f2.read(bytes_per_read)
                    if data != data2:
                        equal = False
                        break

                if equal:
                    tail = file_size % bytes_per_read
                    if tail > 0:
                        data = f.read(tail)
                        data2 = f2.read(tail)
                        if data != data2:
                            equal = False
    except Exception:
        log.exception('compare files ... exception ... exit')
        sys.exit(1)

    if equal:
        log.debug('compare files ... yes')
    else:
        log.debug('compare files ... no')

    return equal

def get_file_sha512(path, bytes_per_read=4*1024*1024):
    """生成文件的SHA512
    """

    if (not isinstance(path, str)
            or not path
            or not os.path.isfile(path)
            or not isinstance(bytes_per_read, int)
            or bytes_per_read <= 0):
        log.critical('get file sha512 ... failed ... exit | illegal input')
        sys.exit(1)

    if os.path.getsize(path) <= 0:
        log.warning('get file sha512 ... ignored | empty file: %s', path)
        return ''

    log.debug('get file sha512 ... ongoing | path: %s', path)

    try:
        with open(path, 'rb') as f:
            m = hashlib.sha512()

            while True:
                buffer = f.read(bytes_per_read)

                if buffer:
                    m.update(buffer)
                else:
                    break

            md = m.hexdigest()
    except Exception:
        log.exception('get file sha512 ... exception ... exit')
        sys.exit(1)

    log.debug('get file sha512 ... successed')
    return md

def save_to_json(path, info, compact=False):
    """保存信息到json格式文本文件
    """

    if (not isinstance(path, str)
            or not path
            or not isinstance(info, dict)
            or not info):
        log.critical('save to json ... failed ... exit | illegal input')
        sys.exit(1)

    try:
        with open(path, 'w', encoding='utf-8') as f:
            if compact:
                json.dump(info, f, separators=(',', ':'))
            else:
                json.dump(info, f, indent='\t')
    except Exception:
        log.exception('save to json ... exception')
        return False

    log.debug('save to json ... successed | compact: %s', compact)
    return True

def load_json(path, restore=None):
    """从json文本文件中加载信息
    """

    if (not isinstance(path, str)
            or not path
            or not os.path.isfile(path)):
        log.critical('load json ... failed ... exit | illegal input')
        sys.exit(1)

    info = {}

    try:
        with open(path, 'r', encoding='utf-8') as f:
            info = json.load(f)

            if callable(restore):
                info = restore(info)
    except Exception:
        log.exception('load json ... exception')
        return {}

    log.debug('load json ... successed')
    return info

def get_json(src, restore=None):
    """从本地json文本文件或者HTTP/HTTPS下载的json文本中获取信息
    """

    if not isinstance(src, str) or not src:
        log.critical('get json ... failed ... exit | illegal input')
        sys.exit(1)

    if os.path.isfile(src):
        info = load_json(src, restore)
    else:
        if not is_supported_url(src):
            log.error('get json ... failed | invalid input')
            return {}

        try:
            with urllib.request.urlopen(src) as response:
                with tempfile.TemporaryDirectory() as temp_dir:
                    path = os.path.join(temp_dir, 'remote.json')

                    with open(path, 'wb') as f:
                        shutil.copyfileobj(response, f)

                    info = load_json(path, restore)
        except Exception:
            log.exception('get json ... exception')
            return {}

    if info:
        log.debug('get json ... successed')
    else:
        log.debug('get json ... failed')

    return info

def run(cmd, trace=True):
    if not isinstance(cmd, str) or not cmd:
        log.critical('run ... failed ... exit | illegal input')
        sys.exit(1)

    if trace:
        log.debug('run ... ongoing | cmd: %s', cmd)

    try:
        exit_code = subprocess.call(cmd,
                                    shell=True,
                                    stdout=subprocess.DEVNULL,
                                    stderr=subprocess.DEVNULL)
    except Exception:
        log.exception('run ... exception')
        return False

    if exit_code:
        log.error('run ... failed | exit code: %d', exit_code)
    else:
        log.debug('run ... successed')

    return not exit_code

def truncate(str_, length, unique=False, seperator='_'):
    if (not isinstance(str_, str)
            or not str_
            or not isinstance(length, int)
            or length < 0
            or not isinstance(seperator, str)):
        log.error('truncate ... failed | illegal input')
        return ''

    if len(str_) > length:
        log.debug('truncate ... ongoing | truncate')
        str_ = str_[:length]

    if unique:
        str_ = '%s%s%s' % (str_, seperator, uuid.uuid1().hex)

    log.debug('truncate ... done')
    return str_

def is_printable_ascii(str_):
    """是否是由可打印的ASCII字符构成的字符串

    可打印的ASCII字符一共是95个字符：0-9a-zA-Z `~!@#$%^&*()-_=+[{}]\|;:'",<.>/?
    """

    if not isinstance(str_, str) or not str_:
        log.error('is printable ascii ... no | illegal input')
        return False

    m = re.search(r'[^'
                  r'0-9a-zA-Z'
                  r'\[\]\^\-\~\&\|\/\{\}\ \\'
                  r'''`!@#$%*()_=+;:'",<.>?'''
                  r']',
                  str_)
    if m and m.group():
        log.warning('is printable ascii ... no | can only contain: %s',
                    '0-9 a-z A-Z space ` ~ ! @ # $ % ^ & * ( ) - _ = + [ { } ] '
                    '''\\ | ; : ' " , < . > / ?''')
        return False

    log.debug('is printable ascii ... yes')
    return True

def is_identifier_ascii(str_):
    """是否是由可做标志符的ASCII字符构成的字符串

    数字字符，字母表字符，外加下划线、短杠和点号，一共65个字符：0-9a-zA-Z_-.
    """

    if not isinstance(str_, str) or not str_:
        log.error('is identifier ascii ... no | illegal input')
        return False

    m = re.search(r'[^0-9a-zA-Z_.\-]', str_)
    if m and m.group():
        log.warning('is identifier ascii ... no | can only contain: %s',
                    '0-9 a-z A-Z _ - .')
        return False

    log.debug('is identifier ascii ... yes')
    return True

def is_hex_ascii(str_):
    """是否是由十六进制的ASCII字符构成的字符串

    一共22个字符：0-9a-fA-F
    """

    if not isinstance(str_, str) or not str_:
        log.error('is hex ascii ... no | illegal input')
        return False

    m = re.search(r'[^0-9a-fA-F]', str_)
    if m and m.group():
        log.warning('is hex ascii ... no | can only contain: %s',
                    '0-9 a-f A-F')
        return False

    log.debug('is hex ascii ... yes')
    return True

def is_hex_ascii2(data):
    """是否是由十六进制的ASCII字符构成的字节串

    一共22个字符：0-9a-fA-F
    """

    if not isinstance(data, bytes) or not data:
        log.error('is hex ascii ... no | illegal input')
        return False

    m = re.search(rb'[^0-9a-fA-F]', data)
    if m and m.group():
        log.warning('is hex ascii ... no | can only contain: %s',
                    '0-9 a-f A-F')
        return False

    log.debug('is hex ascii ... yes')
    return True

def check_host(host):
    """检查主机部分格式

    不含“:”时认为是正确的，含有“:”时要满足特定的格式才认为是正确的
    """

    if not isinstance(host, str) or not host:
        log.error('check host ... error | illegal input')
        return False

    m = re.search(r'\:', host)
    if not m or not m.group():
        log.debug('check host ... ok')
        return True

    m = re.search(r'^[^:]+\:\s*[0-9]+$', host)
    if m and m.group():
        log.debug('check host ... ok')
        return True

    log.error('check host ... error')
    return False

def check_virtual_path(vitrual_path):
    """检查虚拟路径部分格式

    如果含有“//”则认为是不正确的，其它情况都认为正确
    """

    if not isinstance(vitrual_path, str) or not vitrual_path:
        log.error('check virtual path ... error | illegal input')
        return False

    m = re.search(r'\/\/', vitrual_path)
    if m and m.group():
        log.error('check virtual path ... error')
        return False

    log.debug('check virtual path ... ok')
    return True

def is_supported_url(url):
    """判断是否为支持的URL

    这里只接受HTTP与HTTPS两种类型的URL，只检查这两种类型URL的格式
    """

    if not isinstance(url, str) or not url:
        log.error('is supported url ... no | illegal input')
        return False

    m = re.search(r'^https?\:\/\/([^\/]+)\/([^\/].*)$', url, re.I)
    if m and m.group() and m.group(1) and m.group(2):
        log.debug(
            'is supported url ... ongoing | host: %s, vitrual path: %s',
            m.group(1),
            m.group(2))

        if not check_host(m.group(1)):
            log.error('is supported url ... no | host')
            return False

        if not check_virtual_path(m.group(2)):
            log.error('is supported url ... no | virtual path')
            return False

        log.debug('is supported url ... yes')
        return True

    m = re.search(r'^https?\:\/\/([^\/]+)\/?$', url, re.I)
    if m and m.group() and m.group(1):
        log.debug('is supported url ... ongoing | host: %s', m.group(1))

        if not check_host(m.group(1)):
            log.error('is supported url ... no | host')
            return False

        log.debug('is supported url ... yes')
        return True

    log.error('is supported url ... no')
    return False

def get_url_root(url):
    """去除URL虚拟路径部分，返回余下的部分
    """

    if not is_supported_url(url):
        log.error('get url root ... failed | not supported url')
        return ''

    m = re.search(r'^https?\:\/\/[^\/]+', url, re.I)
    if m and m.group():
        root = m.group()
        log.debug('get url root ... successed | root: %s', root)
        return root

    log.error('get url root ... failed | should not happen')
    return ''

def get_relative_url_head(url):
    """从URL获取相对路径头
    """

    if not is_supported_url(url):
        log.error('get relative url head ... failed | not supported url')
        return ''

    m = re.search(r'^(https?\:\/\/.+)\/', url, re.I)
    if m and m.group() and m.group(1):
        relative_url_head = m.group(1)
        log.debug('get relative url head ... successed | relative url head: %s',
                  relative_url_head)
        return relative_url_head

    log.warning('get relative url head ... successed | should not happen')
    return url

def complete_url(url, url_root, relative_url_head):
    """如果url不是完整URL，则补全为完整URL
    """

    if (not isinstance(url, str)
            or not url
            or not is_supported_url(url_root)
            or not is_supported_url(relative_url_head)):
        log.error('complete url ... failed | illegal input')
        return ''

    m = re.search(r'^http', url, re.I)
    if m and m.group():
        log.debug('complete url ... successed | pass')
        return url

    if url.startswith('/'):
        log.debug('complete url ... successed | root')
        return url_root + url

    log.debug('complete url ... successed | head')
    return relative_url_head + '/' + url

def is_supported_filename(filename):
    if not isinstance(filename, str) or not filename:
        log.error('is supported filename ... no | illegal input')
        return False

    m = re.search(r'[:*?"<>\|\/\\]', filename)
    if m and m.group():
        log.error('is supported filename ... no | can not contain: %s',
                  '\\ / : * ? " < > |')
        return False

    log.debug('is supported filename ... yes')
    return True

def is_supported_password(password):
    """是否是可支持的密码字符

    密码限制为95个可打印字符：0-9a-zA-Z `~!@#$%^&*()-_=+[{}]\|;:'",<.>/?
    """

    if not isinstance(password, str):
        log.error('is supported password ... no | illegal input')
        return False

    if password and not is_printable_ascii(password):
        log.error('is supported password ... no')
        return False

    log.debug('is supported password ... yes')
    return True

def has_extension(name, extension):
    """确定扩展名

    确定指定名字中是否具有指定的扩展名，不区分扩展名大小写
    """

    if (not isinstance(name, str)
            or not name
            or not isinstance(extension, str)
            or not extension):
        log.error('has extension ... no | illegal input')
        return False

    if name.lower().endswith(extension.lower()):
        if name.lower() == extension.lower():
            log.warning('has extension ... ongoing | %s', name)

        log.debug('has extension ... yes')
        return True

    log.debug('has extension ... no')
    return False

def has_extension2(name, extension):
    """确定扩展名

    确定指定名字中是否具有指定的那些扩展名之一，不区分扩展名大小写
    """

    if (not isinstance(name, str)
            or not name
            or not isinstance(extension, list)
            or not extension):
        log.error('has extension ... no | illegal input')
        return False

    for e in extension:
        if has_extension(name, e):
            log.debug('has extension ... yes')
            return True

    log.debug('has extension ... no')
    return False

def is_hide(name):
    """检查名字是否以“.”开头
    """

    if isinstance(name, str) and name.startswith('.'):
        log.info('is hide ... yes')
        return True
    else:
        log.debug('is hide ... no')
        return False

def strip_hide_dir(root, dirs):
    """剥除目录列表中以“.”开头的目录
    """

    if not isinstance(root, str) or not root:
        log.error('strip hide dir ... failed | illegal input')
        return dirs

    if not isinstance(dirs, list) or not dirs:
        log.warning('strip hide dir ... ignored | invalid input')
        return dirs

    # 倒序遍历列表删除项
    for i in range(len(dirs) - 1, -1, -1):
        if is_hide(dirs[i]):
            log.info('strip hide dir ... ongoing | strip: %s',
                     os.path.join(root, dirs[i]))
            del dirs[i]

    log.debug('strip hide dir ... successed')
    return dirs

def is_requested_file_by_extension(root, filename, include_extension,
                                   extension):
    """根据对扩展名要求判断是不是需要的文件

    扩展名不区分大小写

    如果是要求包含指定扩展名，会先认为不是需要的文件，然后能确定文件包含了指定扩展名，才认为是
    需要的文件

    如果是要求排除指定扩展名，会先认为是需要的文件，然后能确定文件包含了指定扩展名，才认为不是
    需要的文件
    """

    if (not isinstance(root, str)
            or not root
            or not isinstance(filename, str)
            or not filename):
        log.critical('is requested file by extension ... error ... exit | '
                     'illegal input')
        sys.exit(1)

    if not isinstance(extension, list) or not extension:
        log.warning('is requested file by extension ... yes | empty extension')
        return True

    path = os.path.join(root, filename)
    if include_extension:
        for e in extension:
            if not isinstance(e, str) or not e:
                log.warning('is requested file by extension ... ongoing | '
                            'invalid extension')
                continue

            if filename.lower().endswith(e.lower()):
                log.debug('is requested file by extension ... yes | hit: %s',
                          path)
                return True

        log.debug('is requested file by extension ... no')
        return False
    else:
        for e in extension:
            if not isinstance(e, str) or not e:
                log.warning('is requested file by extension ... ongoing | '
                            'invalid extension')
                continue

            if filename.lower().endswith(e.lower()):
                log.debug('is requested file by extension ... no')
                return False

        log.debug('is requested file by extension ... yes | hit: %s', path)
        return True

def is_requested_file_by_pattern(root, filename, exclude_pattern, pattern):
    """根据搜索字符串结果判断是不是需要的文件

    搜索不区分大小写

    如果是要求包含指定字符串，会先认为不是需要的文件，然后能确定文件包含了指定字符串，才认为是
    需要的文件

    如果是要求不包含指定字符串，会先认为是需要的文件，然后能确定文件包含了指定字符串，才认为不
    是需要的文件
    """

    if (not isinstance(root, str)
            or not root
            or not isinstance(filename, str)
            or not filename):
        log.critical('is requested file by pattern ... error ... exit | '
                     'illegal input')
        sys.exit(1)

    if not isinstance(pattern, list) or not pattern:
        log.warning('is requested file by pattern ... yes | empty pattern')
        return True

    path = os.path.join(root, filename)
    if exclude_pattern:
        for e in pattern:
            if not isinstance(e, str) or not e:
                log.warning('is requested file by pattern ... ongoing | '
                            'invalid pattern')
                continue

            if e.lower() in filename.lower():
                log.debug('is requested file by pattern ... no')
                return False

        log.debug('is requested file by pattern ... yes | hit: %s', path)
        return True
    else:
        for e in pattern:
            if not isinstance(e, str) or not e:
                log.warning('is requested file by pattern ... ongoing | '
                            'invalid pattern')
                continue

            if e.lower() in filename.lower():
                log.debug('is requested file by pattern ... yes | hit: %s',
                          path)
                return True

        log.debug('is requested file by pattern ... no')
        return False

def choose_file_by_extension(root, files, ignore_hide, extension):
    """按扩展名选择文件
    """

    if not isinstance(root, str) or not root:
        log.error('choose file by extension ... failed | illegal input')
        return []

    if not isinstance(files, list) or not files:
        log.warning('choose file by extension ... ignored | invalid input')
        return []

    requested = []
    for filename in files:
        path = os.path.join(root, filename)
        if is_hide(filename) and ignore_hide:
            log.info('choose file by extension ... ongoing | ignore: %s', path)
            continue

        if has_extension(filename, extension):
            requested.append(path)
            log.debug('choose file by extension ... ongoing | hit: %s', path)
            continue

        log.debug('choose file by extension ... ongoing | not hit: %s', path)

    log.debug('choose file by extension ... successed')
    return requested

def count_file_by_extension(root, files, ignore_hide, extension):
    """按扩展名统计文件数量
    """

    if not isinstance(root, str) or not root:
        log.error('count file by extension ... failed | illegal input')
        return 0

    if not isinstance(files, list) or not files:
        log.warning('count file by extension ... ignored | invalid input')
        return 0

    requested = 0
    for filename in files:
        path = os.path.join(root, filename)
        if is_hide(filename) and ignore_hide:
            log.info('count file by extension ... ongoing | ignore: %s', path)
            continue

        if has_extension(filename, extension):
            requested += 1
            log.debug('count file by extension ... ongoing | hit: %s', path)
            continue

        log.debug('count file by extension ... ongoing | not hit: %s', path)

    log.debug('count file by extension ... successed')
    return requested

def choose_empty_file(root, files, ignore_hide):
    """选取空文件
    """

    if not isinstance(root, str) or not root:
        log.error('choose empty file ... failed | illegal input')
        return []

    if not isinstance(files, list) or not files:
        log.warning('choose empty file ... ignored | invalid input')
        return []

    requested = []
    for filename in files:
        path = os.path.join(root, filename)
        if is_hide(filename) and ignore_hide:
            log.info('choose empty file ... ongoing | ignore: %s', path)
            continue

        if os.path.getsize(path) <= 0:
            requested.append(path)
            log.debug('choose empty file ... ongoing | hit: %s', path)
            continue

        log.debug('choose empty file ... ongoing | not hit: %s', path)

    log.debug('choose empty file ... successed')
    return requested

def count_empty_file(root, files, ignore_hide):
    """统计空文件数量
    """

    if not isinstance(root, str) or not root:
        log.error('count empty file ... failed | illegal input')
        return 0

    if not isinstance(files, list) or not files:
        log.warning('count empty file ... ignored | invalid input')
        return 0

    requested = 0
    for filename in files:
        path = os.path.join(root, filename)
        if is_hide(filename) and ignore_hide:
            log.info('count empty file ... ongoing | ignore: %s', path)
            continue

        if os.path.getsize(path) <= 0:
            requested += 1
            log.debug('count empty file ... ongoing | hit: %s', path)
            continue

        log.debug('count empty file ... ongoing | not hit: %s', path)

    log.debug('count empty file ... successed')
    return requested

def make_statistics(info, tristate, total, ignored, failed):
    """按三态结果做统计
    """

    if (not isinstance(info, str)
            or not isinstance(total, list)
            or not total
            or not isinstance(total[0], int)
            or not isinstance(ignored, list)
            or not isinstance(failed, list)):
        log.critical('make statistics ... failed ... exit | illegal input')
        sys.exit(1)

    total[0] += 1

    if tristate is None:
        ignored.append(info)
    elif not tristate:
        failed.append(info)

def remove_file_in_sandbox(path):
    """删除单个文件

    删除单个文件，会拦截导致的任何异常，防止直接退出，以免影响别的任务
    """

    if not isinstance(path, str) or not path or not os.path.isfile(path):
        log.error('remove file in sandbox ... failed | illegal input')
        return False

    try:
        os.remove(path)
    except Exception:
        log.exception('remove file in sandbox ... exception')
        return False

    log.debug('remove file in sandbox ... successed')
    return True

def locate_response_separator(data):
    """定位响应中响应体的位置

    带“-i”选项运行“curl”命令会在响应中包含响应头，响应头与响应体之间有一个空行，此接口据此定
    位响应体的位置
    """

    if not isinstance(data, bytes) or not data:
        log.error('locate response separator ... failed | illegal input')
        return -1

    m = re.search(rb'\r\n\r\n', data)
    if m and m.group():
        log.debug('locate response separator ... successed')
        return m.start() + 2

    log.error('locate response separator ... failed')
    return -1

def get_head_data(data):
    """获取响应头

    结果中不包括响应头与响应体之间作为分隔的那个换行符
    """

    if not isinstance(data, bytes) or not data:
        log.error('get head data ... failed | illegal input')
        return b''

    separator_pos = locate_response_separator(data)
    if separator_pos != -1:
        log.debug('get head data ... successed')
        return data[: separator_pos]

    log.error('get head data ... failed')
    return b''

def check_status_code_206(data):
    """检查206号响应码是否可接受为正常状态码

    Content-Range头表示在大小与Content-Length表示的长度一样，则认为是正常状态码

    格式：

    Content-Range: <unit> <range-start>-<range-end>/<size>
    Content-Range: <unit> <range-start>-<range-end>/*
    Content-Range: <unit> */<size>

    Content-Length: <length>
    """

    if not isinstance(data, bytes) or not data:
        log.error('check status code 206 ... failed | illegal input')
        return False

    size = None
    m = re.search(
        rb'content\-range\:\s*bytes\s+\*\s*\/\s*([0-9]+)\s*\r\n',
        data,
        re.I)
    if m and m.group() and m.group(1):
        size = m.group(1)

    if size is None:
        m = re.search(
            rb'content\-range\:\s*bytes\s+[0-9]+?\s*\-\s*[0-9]+?\s*\/'
            rb'\s*([0-9]+)\s*\r\n',
            data,
            re.I)
    if m and m.group() and m.group(1):
        size = m.group(1)

    if not isinstance(size, bytes) or not size:
        log.error('check status code 206 ... error | size')
        return False

    size = int(size)
    log.debug('check status code 206 ... ongoing | size: %d', size)

    length = None
    m = re.search(rb'content\-length\:\s*([0-9]+)\s*\r\n',
                  data,
                  re.I)
    if m and m.group() and m.group(1):
        length = m.group(1)

    if not isinstance(length, bytes) or not length:
        log.error('check status code 206 ... error | length')
        return False

    length = int(length)
    log.debug('check status code 206 ... ongoing | length: %d', length)

    if size == length:
        log.debug('check status code 206 ... ok')
        return True

    log.error('check status code 206 ... error')
    return False

def check_status_code(data):
    """检查响应头中响应状态码是否是正常状态码

    带“-i”选项运行“curl”命令会在响应中包含响应头，此接口检查响应头中响应状态码是否是正常状
    态码
    """

    if not isinstance(data, bytes) or not data:
        log.error('check status code ... failed | illegal input')
        return False

    data = get_head_data(data)
    if not data:
        log.error('check status code ... error | head')
        return False

    status_code = None
    m = re.search(rb'^\s*HTTP[0-9.\/]+\s+([0-9]+)\s', data, re.I)
    if m and m.group() and m.group(1):
        status_code = m.group(1)

    if status_code is None:
        m = re.search(rb'^\s*HTTP[0-9.\/]+\s+([0-9]+)\s*\r\n', data, re.I)
        if m and m.group() and m.group(1):
            status_code = m.group(1)

    if not isinstance(status_code, bytes) or not status_code:
        log.error('check status code ... error | status code')
        return False

    status_code = int(status_code)
    if status_code == 200:
        log.debug('check status code ... ok | status code: %d', status_code)
        return True
    elif status_code == 206:
        if check_status_code_206(data):
            log.debug('check status code ... ok | status code: %d', status_code)
            return True

    log.error('check status code ... error | status code: %d', status_code)
    return False

def get_body_data(data):
    """从响应中获取响应体

    带“-i”选项运行“curl”命令会在响应中包含响应头，响应头与响应体之间有一个空行，此接口据此获
    取响应体
    """

    if not isinstance(data, bytes) or not data:
        log.error('get body data ... failed | illegal input')
        return b''

    if check_status_code(data):
        separator_pos = locate_response_separator(data)
        if separator_pos != -1:
            log.debug('get body data ... successed')
            return data[separator_pos + 2 :]
        else:
            log.error('get body data ... failed')
            return b''
    else:
        log.error('get body data ... failed')
        return b''

def check_response(response_path, path, bytes_per_read=4*1024*1024):
    """检查响应，如果正常，则提取响应体

    带“-i”选项运行“curl”命令会在响应中包含响应头，响应头与响应体之间有一个空行，此接口据此检
    查并提取响应体
    """

    if not isinstance(response_path, str) or not os.path.isfile(response_path):
        log.error('check response ... error | illegal input')
        return False

    file_size = os.path.getsize(response_path)
    if file_size <= 0:
        log.error('check response ... error | file size: %d', file_size)
        return False

    try:
        with open(response_path, 'rb') as f, open(path, 'wb') as f2:
            if file_size < bytes_per_read:
                body = get_body_data(f.read())
                if not body:
                    log.error('check response ... error')
                    return False

                f2.write(body)
                log.debug('check response ... ok')
                return True
            else:
                times = file_size // bytes_per_read
                for i in range(times):
                    if i == 0:
                        body = get_body_data(f.read(bytes_per_read))
                        if not body:
                            log.error('check response ... error')
                            return False

                        f2.write(body)
                    else:
                        f2.write(f.read(bytes_per_read))

                tail = file_size % bytes_per_read
                if tail > 0:
                    f2.write(f.read(tail))

                log.debug('check response ... ok')
                return True
    except Exception:
        log.exception('check response ... exception')
        return False

def fetch_file_by_curl(curl, url, path):
    """使用外部“curl”命令从网上取文件并保存到本地

    由于这个命令的退出码并不能完美表征错误，所以当退出码是0时，也会出现下载到的文件不是想要的文
    件的情况。所以会带“-i”选项来运行“curl”命令，以在响应中包含响应头，然后根据响应头中的状态
    码是否为“2xx”来判断是否成功执行了下载任务
    """

    response_path = path + '.curlresponse'
    cmd = '"%s" -i -o "%s" "%s"' % (curl, response_path, url)
    log.debug('fetch file by curl ... ongoing | launch: %s', cmd)

    try:
        output_path = path + '.curlconsole'
        with open(output_path, 'wb') as f:
            exit_code = subprocess.call(cmd, shell=True, stdout=f, stderr=f)
    except Exception:
        log.exception('fetch file by curl ... exception')
        return False

    log.debug('fetch file by curl ... ongoing | exit code: %d', exit_code)
    if exit_code == 0:
        if check_response(response_path, path):
            log.debug('fetch file by curl ... successed | url: %s', url)
            return True

    log.error('fetch file by curl ... failed | url: %s', url)
    return False

def do_with_random_retry(func, retry, interval, *args):
    """带随机间隔重试的执行指定任务
    """

    if (not callable(func)
            or not isinstance(retry, int)
            or not isinstance(interval, list)
            or not interval):
        log.critical('do with random retry ... failed ... exit | illegal input')
        sys.exit(1)

    countdown = retry
    while True:
        yes = func(*args)

        if not yes and countdown > 0:
            countdown -= 1

            random_interval = random.choice(interval)
            log.warning(
                'do with random retry ... ongoing | waitting %ds for retry',
                random_interval)
            time.sleep(random_interval)
        else:
            break

    if yes:
        log.debug('do with random retry ... successed | retry: %d',
                  retry - countdown)
    else:
        log.error('do with random retry ... failed')

    return yes

def do_with_retry(func, retry, *args):
    """带随机间隔重试的执行指定任务
    """

    if not callable(func) or not isinstance(retry, int):
        log.critical('do with retry ... failed ... exit | illegal input')
        sys.exit(1)

    countdown = retry
    while True:
        yes = func(*args)

        if not yes and countdown > 0:
            countdown -= 1

            log.warning('do with retry ... ongoing | retry')
        else:
            break

    if yes:
        log.debug('do with retry ... successed | retry: %d',
                  retry - countdown)
    else:
        log.error('do with retry ... failed')

    return yes

def input_with_sha512(description, times, check_func, sha512):
    if (not isinstance(description, str)
            or not description
            or not isinstance(times, int)
            or not callable(check_func)
            or not isinstance(sha512, str)
            or len(sha512) != 128):
        log.critical('input with sha512 ... failed ... exit | illegal input')
        sys.exit(1)

    countdown = times
    while countdown > 0:
        countdown -= 1

        password = getpass.getpass('Please input %s: ' % description)

        m = hashlib.sha512()
        m.update(password.encode('utf-8'))
        password2 = m.hexdigest()

        if password2.lower() != sha512.lower():
            if countdown > 1:
                prompt('Incorrect, %d chances left, please input again!'
                       % countdown)
            elif countdown == 1:
                prompt('Incorrect, 1 chance left, please input again!')
            else:
                prompt('Incorrect, exit!')

                log.critical('input with sha512 ... failed ... exit')
                sys.exit(1)
        elif not check_func(password):
            if countdown > 1:
                prompt('Not supported %s, %d chances left, please input again!'
                       % (description, countdown))
            elif countdown == 1:
                prompt('Not supported %s, 1 chances left, please input again!'
                       % description)
            else:
                prompt('Not supported %s, exit!' % description)

                log.critical('input with sha512 ... failed ... exit')
                sys.exit(1)
        else:
            break

    log.info('input with sha512 ... successed | empty: %s', not password)
    return password

def input_double(description, times, check_func):
    if (not isinstance(description, str)
            or not description
            or not isinstance(times, int)
            or not callable(check_func)):
        log.critical('input double ... failed ... exit | illegal input')
        sys.exit(1)

    countdown = times
    while countdown > 0:
        countdown -= 1

        password = getpass.getpass('Please input %s: ' % description)
        password2 = getpass.getpass('Please input %s again: ' % description)

        if password != password2:
            if countdown > 1:
                prompt('Not the same, %d chances left, please input again!'
                       % countdown)
            elif countdown == 1:
                prompt('Not the same, 1 chance left, please input again!')
            else:
                prompt('Not the same, exit!')

                log.critical('input double ... failed ... exit')
                sys.exit(1)
        elif not check_func(password):
            if countdown > 1:
                prompt('Not supported %s, %d chances left, please input again!'
                       % (description, countdown))
            elif countdown == 1:
                prompt('Not supported %s, 1 chances left, please input again!'
                       % description)
            else:
                prompt('Not supported %s, exit!' % description)

                log.critical('input double ... failed ... exit')
                sys.exit(1)
        else:
            break

    log.info('input double ... successed | empty: %s', not password)
    return password

def get_os_password_for_mac(password_sha512):
    if (not isinstance(password_sha512, str)
            and not isinstance(password_sha512, bool)):
        log.critical('get os password for mac ... failed ... exit | illegal '
                     'input')
        sys.exit(1)

    if not password_sha512:
        log.info('get os password for mac ... ongoing | no check')
        return input_double('OS password', 3, is_supported_password)
    else:
        log.info('get os password for mac ... ongoing | check')
        return input_with_sha512('OS password', 3, is_supported_password,
                                 password_sha512)

def get_os_password(password_sha512):
    if (password_sha512 is not None
            and not isinstance(password_sha512, str)
            and not isinstance(password_sha512, bool)):
        log.critical('get os password ... failed ... exit | illegal input')
        sys.exit(1)

    if password_sha512 is None:
        log.info('get os password ... ignored')
        return None

    if platform.system() == 'Darwin':
        return get_os_password_for_mac(password_sha512)

    log.warning('get os password ... ignored | not supported platform')
    return None

def shutdown_for_mac(password):
    if password is not None and not isinstance(password, str):
        log.critical('shutdown for mac ... failed ... exit | illegal input')
        sys.exit(1)

    cmd = ('printf "%s\\n" | sudo -S shutdown -h +1'
           % ''.join(['\\' + ('%o' % c) for c in password.encode('utf-8')]))

    if run(cmd, False):
        log.info('shutdown for mac ... successed')
    else:
        log.error('shutdown for mac ... failed')

def shutdown(password):
    if password is not None and not isinstance(password, str):
        log.critical('shutdown ... failed ... exit | illegal input')
        sys.exit(1)

    if password is None:
        log.info('shutdown ... ignored')
        return

    if platform.system() == 'Darwin':
        shutdown_for_mac(password)

def arg_type_positive(str_):
    try:
        positive = int(str_)
    except Exception:
        msg = 'not int: %s' % str_
        raise argparse.ArgumentTypeError(msg)

    if positive <= 0:
        msg = 'should > 0'
        raise argparse.ArgumentTypeError(msg)

    return positive

def arg_type_positive2(str_, min=5):
    if not isinstance(min, int) or min <= 0:
        msg = 'type conversion call failed: illegal input'
        raise argparse.ArgumentTypeError(msg)

    try:
        positive = int(str_)
    except Exception:
        msg = 'not int: %s' % str_
        raise argparse.ArgumentTypeError(msg)

    if positive < min:
        msg = 'should >= %d' % min
        raise argparse.ArgumentTypeError(msg)

    return positive

def arg_type_nonnegative(str_):
    try:
        nonnegative = int(str_)
    except Exception:
        msg = 'not int: %s' % str_
        raise argparse.ArgumentTypeError(msg)

    if nonnegative < 0:
        msg = 'should >= 0'
        raise argparse.ArgumentTypeError(msg)

    return nonnegative

def arg_type_sha512(str_):
    if len(str_) != 128:
        msg = 'should be 128 characters'
        raise argparse.ArgumentTypeError(msg)

    if not is_hex_ascii(str_):
        msg = 'can only contain: 0-9 a-f A-F'
        raise argparse.ArgumentTypeError(msg)

    return str_

def arg_type_path(str_):
    if not os.path.isdir(str_) and not os.path.isfile(str_):
        msg = 'not dir or file: %s' % str_
        raise argparse.ArgumentTypeError(msg)

    return str_

def arg_type_dir(str_):
    if not os.path.isdir(str_):
        msg = 'not dir: %s' % str_
        raise argparse.ArgumentTypeError(msg)

    return str_

def arg_type_file(str_):
    if not os.path.isfile(str_):
        msg = 'not file: %s' % str_
        raise argparse.ArgumentTypeError(msg)

    return str_

def arg_type_file2(str_):
    if not os.path.isfile(str_) and not is_supported_url(str_):
        msg = 'not file or HTTP/HTTPS: %s' % str_
        raise argparse.ArgumentTypeError(msg)

    return str_

def arg_type_filename(str_):
    if not str_:
        msg = 'empty'
        raise argparse.ArgumentTypeError(msg)

    if not is_supported_filename(str_):
        msg = 'can not contain: \\ / : * ? " < > |'
        raise argparse.ArgumentTypeError(msg)

    return str_

def arg_type_identifier(str_):
    if not str_:
        msg = 'empty'
        raise argparse.ArgumentTypeError(msg)

    if not is_identifier_ascii(str_):
        msg = 'can only contain: 0-9 A-Z a-z _ - .'
        raise argparse.ArgumentTypeError(msg)

    return str_

def arg_type_non_empty(str_):
    if not str_:
        msg = 'empty'
        raise argparse.ArgumentTypeError(msg)

    return str_

def arg_type_non_empty2(str_):
    if not str_:
        msg = 'empty item'
        raise argparse.ArgumentTypeError(msg)

    return str_

def arg_type_url(str_):
    if not str_:
        msg = 'empty'
        raise argparse.ArgumentTypeError(msg)

    if not is_supported_url(str_):
        msg = 'not supported url'
        raise argparse.ArgumentTypeError(msg)

    return str_

def arg_type_mb_per_read(str_):
    try:
        mb_per_read = int(str_)
    except Exception:
        msg = 'not int: %s' % str_
        raise argparse.ArgumentTypeError(msg)

    if mb_per_read < 1 or mb_per_read > 1024:
        msg = 'should >= 1 and <= 1024'
        raise argparse.ArgumentTypeError(msg)

    return mb_per_read

def arg_type_ffmpeg(str_):
    if not str_:
        msg = 'empty'
        raise argparse.ArgumentTypeError(msg)

    m = re.search(r'ffmpeg$', str_, re.I)
    if not m or not m.group():
        msg = 'incorrect ffmpeg command'
        raise argparse.ArgumentTypeError(msg)

    test_cmd = '"%s" -h' % str_
    if not run(test_cmd):
        msg = 'invalid ffmpeg command'
        raise argparse.ArgumentTypeError(msg)

    return str_

def arg_type_curl(str_):
    if not str_:
        msg = 'empty'
        raise argparse.ArgumentTypeError(msg)

    m = re.search(r'curl$', str_, re.I)
    if not m or not m.group():
        msg = 'incorrect curl command'
        raise argparse.ArgumentTypeError(msg)

    test_cmd = '"%s" -h' % str_
    if not run(test_cmd):
        msg = 'invalid curl command'
        raise argparse.ArgumentTypeError(msg)

    return str_

def arg_log():
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        '-l', '--log',
        choices=['debug', 'info', 'warning', 'error', 'critical'],
        default='info',
        help='控制日志打印级别，有debug、info、warning、error、critical这些值可用，默'
             '认是info级别')

    return parser

def arg_log_identifier():
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument(
        '-L', '--log_identifier',
        type=arg_type_identifier,
        default=None,
        help='在日志文件名中指定一个标志符，方便查找对应日志')

    return parser

def arg_shutdown():
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument('-s', '--shutdown',
                        type=arg_type_sha512,
                        nargs='?',
                        default=None,
                        const=False,
                        help='任务完成后自动关机，只支持特定平台，由于关机需要密码，这选'
                             '项会导致程序启动后会要求输入开机密码，如果这个选项给出开机'
                             '密码的SHA512串，则会使用其来校验密码输入是否正确，如果这'
                             '个选项没有给值，则不校验密码输入是否正确，只是输入两次做'
                             '前后对比校验')

    return parser

def prompt(info, undecorated=False, trace=True):
    """输出规范提示信息
    """

    if not isinstance(info, str):
        if trace:
            log.critical('prompt ... failed ... exit | illegal input')
        sys.exit(1)

    if trace:
        log.info('prompt ... ongoing | info: %s, undecorated: %s',
                 info,
                 bool(undecorated))

    now = datetime.datetime.now()
    if undecorated:
        prompt = info
    else:
        prompt = '[%s]%s' % (now.strftime('%Y-%m-%d %H:%M:%S'), info)
    print(prompt)

def prompt_startup(baseline):
    if not isinstance(baseline, datetime.datetime):
        log.critical('prompt startup ... failed ... exit | illegal input')
        sys.exit(1)

    log.info('prompt startup ... ongoing | baseline: %s',
             baseline.strftime('%Y-%m-%d %H:%M:%S'))

    prompt = '[%s]Startup' % baseline.strftime('%Y-%m-%d %H:%M:%S')
    print(prompt)

def prompt_terminate(baseline, exit_code=0):
    if (not isinstance(baseline, datetime.datetime)
            or not isinstance(exit_code, int)):
        log.critical('prompt terminate ... failed ... exit | illegal input')
        sys.exit(1)

    now = datetime.datetime.now()

    log.info('prompt terminate ... ongoing | baseline: %s, exit code: %d, '
             'cost: %s',
             baseline.strftime('%Y-%m-%d %H:%M:%S'),
             exit_code,
             now - baseline)

    if exit_code:
        prompt = '[%s]A fatal error occured' % now.strftime('%Y-%m-%d %H:%M:%S')
        print(prompt)

    prompt = ('[%s]Terminate, cost: %s'
              % (now.strftime('%Y-%m-%d %H:%M:%S'), now - baseline))
    print(prompt)

def prompt_true(yes, info):
    """满足条件则输出规范提示信息
    """

    if not isinstance(info, str):
        log.critical('prompt true ... failed ... exit | illegal input')
        sys.exit(1)

    log.info('prompt true ... ongoing | yes: %s, info: %s', bool(yes), info)

    if yes:
        now = datetime.datetime.now()
        prompt = '[%s]%s' % (now.strftime('%Y-%m-%d %H:%M:%S'), info)
        print(prompt)

def prompt_false(yes, info):
    """不满足条件则输出规范提示信息退出
    """

    if not isinstance(info, str):
        log.critical('prompt false ... failed ... exit | illegal input')
        sys.exit(1)

    log.info('prompt false ... ongoing | yes: %s, info: %s', bool(yes), info)

    if not yes:
        now = datetime.datetime.now()
        prompt = '[%s]%s' % (now.strftime('%Y-%m-%d %H:%M:%S'), info)
        print(prompt)

        log.critical('prompt false ... failed ... exit')
        sys.exit(1)

def prompt_status(action, status):
    """输出操作进展的规范提示信息
    """

    if not isinstance(action, str) or not isinstance(status, str):
        log.critical('prompt status ... failed ... exit | illegal input')
        sys.exit(1)

    log.info('prompt status ... ongoing | action: %s, status: %s',
             action,
             status)

    now = datetime.datetime.now()
    prompt = ('[%s]%s ... %s'
              % (now.strftime('%Y-%m-%d %H:%M:%S'), action, status))
    print(prompt)

def prompt_assert(yes, action, exit=True):
    """根据断言输出操作结果的规范提示信息
    """

    if not isinstance(action, str):
        log.critical('prompt assert ... failed ... exit | illegal input')
        sys.exit(1)

    log.info(
        'prompt assert ... ongoing | yes: %s, action: %s, exit: %s',
        bool(yes),
        action,
        bool(exit))

    now = datetime.datetime.now()

    if yes:
        prompt = ('[%s]%s ... successed'
                  % (now.strftime('%Y-%m-%d %H:%M:%S'), action))
        print(prompt)
    else:
        prompt = ('[%s]%s ... failed'
                  % (now.strftime('%Y-%m-%d %H:%M:%S'), action))
        print(prompt)

        if exit:
            log.critical('prompt assert ... failed ... exit')
            sys.exit(1)

def prompt_tristate(tristate, action, exit=True):
    """根据三态输出操作结果的规范提示信息
    """

    if not isinstance(action, str):
        log.critical('prompt tristate ... failed ... exit | illegal input')
        sys.exit(1)

    log.info(
        'prompt tristate ... ongoing | tristate: %s, action: %s, exit: %s',
        None if tristate is None else bool(tristate),
        action,
        bool(exit))

    if tristate is None:
        now = datetime.datetime.now()
        prompt = ('[%s]%s ... ignored'
                  % (now.strftime('%Y-%m-%d %H:%M:%S'), action))
        print(prompt)
    else:
        prompt_assert(tristate, action, exit)

def prompt_batch(total, ignored, failed):
    """输出批处理的规范提示信息
    """

    if (not isinstance(total, int)
            or not isinstance(ignored, list)
            or not isinstance(failed, list)):
        log.critical('prompt batch ... failed ... exit | illegal input')
        sys.exit(1)

    log.info('prompt batch ... ongoing | total: %d, ignored: %d, failed: %d',
             total,
             len(ignored),
             len(failed))

    now = datetime.datetime.now()

    if ignored or failed:
        prompt = '[%s]Report:' % now.strftime('%Y-%m-%d %H:%M:%S')
        print(prompt)

    if ignored:
        log.info('prompt batch ... ongoing | igonre')
        print('Ignored:')
    for info in ignored:
        if isinstance(info, str):
            log.info('prompt batch ... ongoing | %s', info)
            print(info)

    if failed:
        log.info('prompt batch ... ongoing | failed')
        print('Failed:')
    for info in failed:
        if isinstance(info, str):
            log.info('prompt batch ... ongoing | %s', info)
            print(info)

    if not ignored and not failed:
        prompt = ('[%s]Total %d, successed %d'
                  % (now.strftime('%Y-%m-%d %H:%M:%S'), total, total))
    elif ignored and not failed:
        prompt = ('[%s]Total %d, ignored %d'
                  % (now.strftime('%Y-%m-%d %H:%M:%S'), total, len(ignored)))
    elif not ignored and failed:
        prompt = ('[%s]Total %d, failed %d'
                  % (now.strftime('%Y-%m-%d %H:%M:%S'), total, len(failed)))
    else:
        prompt = ('[%s]Total %d, ignored: %d, failed %d'
                  % (now.strftime('%Y-%m-%d %H:%M:%S'),
                     total,
                     len(ignored),
                     len(failed)))
    print(prompt)

def prompt_batch2(total):
    """输出批处理的规范提示信息

    total的类型，要么是int，要么是list
    """

    if not isinstance(total, int) and not isinstance(total, list):
        log.critical('prompt batch ... failed ... exit | illegal input')
        sys.exit(1)

    log.info('prompt batch ... ongoing | total: %d',
             total if isinstance(total, int) else len(total))

    now = datetime.datetime.now()

    if isinstance(total, list):
        if total:
            prompt = '[%s]Report:' % now.strftime('%Y-%m-%d %H:%M:%S')
            print(prompt)

        for info in total:
            if isinstance(info, str):
                log.info('prompt batch ... ongoing | %s', info)
                print(info)

    if isinstance(total, list):
        prompt = ('[%s]Total %d'
                  % (now.strftime('%Y-%m-%d %H:%M:%S'), len(total)))
    else:
        prompt = '[%s]Total %d' % (now.strftime('%Y-%m-%d %H:%M:%S'), total)
    print(prompt)

def prompt_batch3(total, failed):
    """输出批处理的规范提示信息
    """

    if not isinstance(total, int) or not isinstance(failed, list):
        log.critical('prompt batch ... failed ... exit | illegal input')
        sys.exit(1)

    log.info('prompt batch ... ongoing | total: %d, failed: %d',
             total,
             len(failed))

    now = datetime.datetime.now()

    if failed:
        log.info('prompt batch ... ongoing | failed')
        prompt = '[%s]Report:' % now.strftime('%Y-%m-%d %H:%M:%S')
        print(prompt)
        print('Failed:')

    for info in failed:
        if isinstance(info, str):
            log.info('prompt batch ... ongoing | %s', info)
            print(info)

    if failed:
        prompt = ('[%s]Total %d, failed %d'
                  % (now.strftime('%Y-%m-%d %H:%M:%S'), total, len(failed)))
    else:
        prompt = ('[%s]Total %d, successed %d'
                  % (now.strftime('%Y-%m-%d %H:%M:%S'), total, total))
    print(prompt)
